練習問題#

ここでは、 本書の学習内容の定着 を目的とした練習問題とその解答・解説を掲載します。 解答・解説に関してはトグルで隠してありますので、学習に役立ててください。 なお、問題の性質上、本書で取り上げた処理と重複することがあります。 ご了承ください。

準備#

以下のように、ライブラリのインポートと変数の定義が完了していることを前提とします。

Hide code cell content
# itertoolsモジュールのインポート
# 様々なパターンのループを効率的に実行可能
import itertools

# pathlibモジュールのインポート
# ファイルシステムのパスを扱う
from pathlib import Path

# numpy:数値計算ライブラリのインポート
# npという名前で参照可能
import numpy as np

# pandas:データ解析ライブラリのインポート
# pdという名前で参照可能
import pandas as pd

# plotly.expressのインポート
# インタラクティブなグラフ作成のライブラリ
# pxという名前で参照可能
import plotly.express as px

# plotly.graph_objectsからFigureクラスのインポート
# 型ヒントの利用を主目的とする
from plotly.graph_objects import Figure
Hide code cell content
# データ格納ディレクトリのパス
DIR_IN = Path("../../../data/cm/input")

# ファイル名の定義
FN_CE = "cm_ce.csv"
FN_CC_CRT = "cm_cc_crt.csv"

# plotlyの描画設定の定義
# Jupyter Notebook環境のグラフ表示に適切なものを選択
RENDERER = "plotly_mimetype+notebook"

また、本書中で取り上げた以下の関数も、同様に利用可能とします。

Hide code cell content
def show_fig(fig: Figure) -> None:
    """
    所定のレンダラーを用いてplotlyの図を表示
    Jupyter Bookなどの環境での正確な表示を目的とする

    Parameters
    ----------
    fig : Figure
        表示対象のplotly図

    Returns
    -------
    None
    """

    # 図の周囲の余白を設定
    # t: 上余白
    # l: 左余白
    # r: 右余白
    # b: 下余白
    fig.update_layout(margin=dict(t=25, l=25, r=25, b=25))

    # 所定のレンダラーで図を表示
    fig.show(renderer=RENDERER)
Hide code cell content
def add_years_to_df(
    df: pd.DataFrame, unit_years: int = 10, col_date: str = "date"
) -> pd.DataFrame:
    """
    データフレームにunit_years単位で区切った年数を示す新しい列を追加

    Parameters
    ----------
    df : pd.DataFrame
        入力データフレーム
    unit_years : int, optional
        年数を区切る単位、デフォルトは10
    col_date : str, optional
        日付を含むカラム名、デフォルトは "date"

    Returns
    -------
    pd.DataFrame
        新しい列が追加されたデータフレーム
    """

    # 入力データフレームをコピー
    df_new = df.copy()

    # unit_years単位で年数を区切り、新しい列として追加
    df_new["years"] = (
        pd.to_datetime(df_new[col_date]).dt.year // unit_years * unit_years
    )

    # 'years'列のデータ型を文字列に変更
    df_new["years"] = df_new["years"].astype(str)

    return df_new
Hide code cell content
def resample_df_by_col_and_years(df: pd.DataFrame, col: str) -> pd.DataFrame:
    """
    指定されたカラムと年数に基づき、データフレームを再サンプル
    colとyearsの全ての組み合わせが存在するように0埋めを行う
    この処理は、作図時にX軸方向の順序が変わることを防ぐために必要

    Parameters
    ----------
    df : pd.DataFrame
        入力データフレーム
    col : str
        サンプリング対象のカラム名

    Returns
    -------
    pd.DataFrame
        再サンプルされたデータフレーム
    """

    # 入力データフレームを新しい変数にコピー
    df_new = df.copy()

    # データフレームからユニークな年数一覧を取得
    unique_years = df["years"].unique()

    # データフレームからユニークなcolの値一覧を取得
    unique_vals = df[col].unique()

    # 一意なカラムの値と年数の全ての組み合わせに対して処理
    for val, years in itertools.product(unique_vals, unique_years):
        # 対象のカラムの値と年数が一致するデータを抽出
        df_tmp = df_new[(df_new[col] == val) & (df_new["years"] == years)]

        # 該当するデータが存在しない場合
        if df_tmp.shape[0] == 0:
            # 0埋めのデータを作成
            default_data = {c: 0 for c in df_tmp.columns}
            # col列についてはvalで埋める
            default_data[col] = val
            # years列についてはyearで埋める
            default_data["years"] = years
            # 新たなレコードとして追加
            df_add = pd.DataFrame(default_data, index=[0])

            # 0埋めのデータをデータフレームに追加
            df_new = pd.concat([df_new, df_add], ignore_index=True)

    return df_new

問題1:合計ページ数の可視化#

本文中のヒートマップでは「年間合計話数」を可視化し、こちら葛飾区亀有公園前派出所の長期にわたる安定した掲載ペースを確認しました。 しかし、マンガ家にとっての負担は「話数」だけではなく、執筆する「ページ数」にも大きく依存します。

df_barで抽出した合計話数上位20作品を対象に、「掲載年」ごとの「合計ページ数」 をヒートマップで可視化してください。 その際、 Y軸の並び順を「連載開始順」 にすることで、各作品が活躍した時代の移り変わりが見えるように工夫してください。

Hide code cell source
# 1. 分析対象の作品リスト(合計話数上位20作品)を作成
# df_ceから作品ごとの話数を集計
df_bar = df_ce.groupby("ccname")["ceid"].nunique().reset_index(name="n_ce")
# 話数が多い順にソートし、上位20件を取得
df_bar = df_bar.sort_values(by="n_ce", ascending=False, ignore_index=True).head(20)
# 作品名のリストを取得
top_ccnames = df_bar["ccname"].unique()

# 2. ヒートマップ表示用に、作品名を「連載開始順(開始日が古い順)」に並び替える
# 上位20作品に絞り込み、作品ごとに最初の日付(min)を取得
series_start_dates = df_ce[df_ce["ccname"].isin(top_ccnames)].groupby("ccname")["date"].min()
# 日付順にソートして作品名のリストを作成
sorted_ccnames = series_start_dates.sort_values().index.tolist()

# 3. ヒートマップ用のデータフレームを作成
# df_ceに掲載年(years)を追加(1年単位)
df_ce_q1 = add_years_to_df(df_ce, unit_years=1)

# 上位20作品のみを抽出
df_hm_pages = df_ce_q1[df_ce_q1["ccname"].isin(top_ccnames)].reset_index(drop=True)

# 「マンガ作品名(ccname)」と「掲載年(years)」で集計し、「pages」の合計を計算
df_hm_pages = (
    df_hm_pages.groupby(["ccname", "years"])["pages"]
    .sum()
    .reset_index(name="total_pages")
)

# 4. データの整形
# 可視化の際に時系列順序が崩れないよう、0埋め処理を実行
df_hm_pages = resample_df_by_col_and_years(df_hm_pages, "ccname")

# ヒートマップを見やすくするため、マンガ作品名を連載開始順(sorted_ccnames)に設定
df_hm_pages["ccname"] = pd.Categorical(
    df_hm_pages["ccname"], categories=sorted_ccnames, ordered=True
)
df_hm_pages = df_hm_pages.sort_values(["ccname", "years"], ignore_index=True)

# 5. ヒートマップを作成
# x軸に掲載年、y軸にマンガ作品名、z軸に合計ページ数を設定
fig = px.density_heatmap(
    df_hm_pages,
    x="years",
    y="ccname",
    z="total_pages",
    height=600,
)

# ヒートマップを表示
show_fig(fig)

問題2:雑誌1冊あたりの合計ページ数の比較#

df_ceを用いて、マンガ雑誌(mcname)ごとの「1冊あたりの総ページ数」の分布を箱ひげ図で可視化してみましょう。 具体的には、巻号ID(miid)ごとに 最終ページ番号(page_endの最大値) を取得し、それを雑誌の総ページ数とみなして可視化を行ってください。

Hide code cell source
# 1. 巻号ごとの総ページ数(最終ページ番号)を集計
# miidとmcnameでグループ化し、page_endの最大値を取得する
# これにより、マンガ以外のページを含んだ雑誌全体の「到達ページ数」を取得できる
df_vol_pages = (
    df_ce.groupby(["miid", "mcname"])["page_end"]
    .max()
    .reset_index(name="total_pages")
)

# 2. 箱ひげ図を作成
# x軸にマンガ雑誌名、y軸に総ページ数を設定
fig = px.box(
    df_vol_pages,
    x="mcname",
    y="total_pages"
)

# 箱ひげ図を表示
show_fig(fig)

問題3:カラー掲載の内訳分析#

本文では、マンガ作品ごとの4色カラー獲得数の違いを分析しました。 しかし、一口に「カラー」と言っても、雑誌の顔となる「巻頭カラー」と雑誌の中ほどに掲載される「センターカラー」では、その意味合いが異なる可能性があります。

df_ceを用いて、4色カラー獲得数が多い上位10作品を抽出し、その内訳(巻頭カラーか、センターカラーか)を積上げ棒グラフで可視化してください。 ここでは便宜上、巻頭カラーはpage_start_positionが0.05以下のカラー回、それ以外をセンターカラーと定義します。

Hide code cell source
# 1. 4色カラーのデータのみを抽出
df_colored = df_ce[df_ce["four_colored"]].reset_index(drop=True)

# 2. 巻頭カラー(0.05以下)かセンターカラーかを判別する列を追加
# apply関数とlambda式を用いて条件分岐させます
df_colored["color_type"] = df_colored["page_start_position"].apply(
    lambda x: "巻頭カラー" if x <= 0.05 else "センターカラー"
)

# 3. 4色カラー獲得数が多い上位10作品を抽出
# value_counts()で作品ごとの回数をカウントし、上位10件のインデックス(作品名)を取得
top10_colored_titles = df_colored["ccname"].value_counts().head(10).index
# 上位10作品のデータのみに絞り込み
df_colored_top10 = df_colored[df_colored["ccname"].isin(top10_colored_titles)]

# 4. 作品名とカラータイプで集計
df_stack = (
    df_colored_top10.groupby(["ccname", "color_type"])
    .size()
    .reset_index(name="count")
)

# 5. 積上げ棒グラフを作成
# x軸に回数、y軸に作品名、色分けにカラータイプを指定
fig = px.bar(
    df_stack,
    x="count",
    y="ccname",
    color="color_type",
    orientation="h",
    barmode="stack",
    labels={"count": "獲得回数", "color_type": "掲載タイプ"}
)

# グラフを表示
show_fig(fig)

問題4:ページ数と掲載位置の関係#

本書では「長期連載作品ほど連載初期にカラーを獲得しやすい」という仮説を検証しました。 これと関連して「カラーページを獲得する回は、通常よりもページ数が多い」気がします。

df_ceを用いて、「ページ数(pages)」と「掲載位置(page_start_position)」の関係を表す散布図を作成してください。 その際、「カラーの有無(four_colored)」で色分けを行い、カラー回が増ページ傾向にあるか、また掲載位置が巻頭寄りであるかを確認しましょう。

※ データ量が多いため、不透明度(opacity)を調整して重なりを見やすくしてください。

Hide code cell source
# 1. データのフィルタリング
# 外れ値の影響を抑えるため、ページ数が50ページ未満のデータに限定して分析
df_scatter_pages = df_ce[df_ce["pages"] < 50].reset_index(drop=True)

# 2. 散布図を作成
# x軸に掲載位置、y軸にページ数を設定
# 色分けにカラー有無(four_colored)を指定
# 重なりを確認するため、opacity(不透明度)を低めに設定
fig = px.scatter(
    df_scatter_pages,
    x="page_start_position",
    y="pages",
    color="four_colored",
    opacity=0.5,
    labels={"four_colored": "4色カラー有無"} # 凡例を見やすく変更
)

# マーカーのサイズ等を調整して見やすくする
fig.update_traces(marker=dict(size=5))

# y軸(ページ数)の範囲を適切に設定(例: 0〜55)
fig.update_yaxes(range=[0, 55])

# 散布図を表示
show_fig(fig)